/*
 * Copyright (C) 2014
 *
 * Brick Yang <printfxxx@163.com>
 *
 * This program is free software. You can redistribute it and/or
 * modify it as you like.
 */

/**
 * @file	compat.c
 * @brief	Compatible kernel APIs implementation
 */

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kallsyms.h>

#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 14, 0)
const char *phy_speed_to_str(int speed)
{
	switch (speed) {
#ifdef SPEED_10
	case SPEED_10:
		return "10Mbps";
#endif
#ifdef SPEED_100
	case SPEED_100:
		return "100Mbps";
#endif
#ifdef SPEED_1000
	case SPEED_1000:
		return "1Gbps";
#endif
#ifdef SPEED_2500
	case SPEED_2500:
		return "2.5Gbps";
#endif
#ifdef SPEED_5000
	case SPEED_5000:
		return "5Gbps";
#endif
#ifdef SPEED_10000
	case SPEED_10000:
		return "10Gbps";
#endif
#ifdef SPEED_14000
	case SPEED_14000:
		return "14Gbps";
#endif
#ifdef SPEED_20000
	case SPEED_20000:
		return "20Gbps";
#endif
#ifdef SPEED_25000
	case SPEED_25000:
		return "25Gbps";
#endif
#ifdef SPEED_40000
	case SPEED_40000:
		return "40Gbps";
#endif
#ifdef SPEED_50000
	case SPEED_50000:
		return "50Gbps";
#endif
#ifdef SPEED_56000
	case SPEED_56000:
		return "56Gbps";
#endif
#ifdef SPEED_100000
	case SPEED_100000:
		return "100Gbps";
#endif
#ifdef SPEED_UNKNOWN
	case SPEED_UNKNOWN:
		return "Unknown";
#endif
	default:
		return "Unsupported";
	}
}
EXPORT_SYMBOL(phy_speed_to_str);

const char *phy_duplex_to_str(unsigned int duplex)
{
	switch (duplex) {
#ifdef DUPLEX_HALF
	case DUPLEX_HALF:
		return "Half";
#endif
#ifdef DUPLEX_FULL
	case DUPLEX_FULL:
		return "Full";
#endif
#ifdef DUPLEX_UNKNOWN
	case DUPLEX_UNKNOWN:
		return "Unknown";
#endif
	default:
		return "Unsupported";
	}
}
EXPORT_SYMBOL(phy_duplex_to_str);
#endif

#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 16, 0)
int fwnode_get_phy_mode(struct fwnode_handle *fwnode)
{
	const char *pm;
	int err, i;

	err = fwnode_property_read_string(fwnode, "phy-mode", &pm);
	if (err < 0)
		err = fwnode_property_read_string(fwnode,
						  "phy-connection-type", &pm);
	if (err < 0)
		return err;

	for (i = 0; i < PHY_INTERFACE_MODE_MAX; i++)
		if (!strcasecmp(pm, phy_modes(i)))
			return i;

	return -ENODEV;
}
EXPORT_SYMBOL_GPL(fwnode_get_phy_mode);
#endif

#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 2, 0)
struct sk_buff *build_skb_around(struct sk_buff *skb,
				 void *data, unsigned int frag_size)
{
	struct skb_shared_info *shinfo;
	unsigned int size = frag_size ? : ksize(data);

	if (unlikely(!skb))
		return NULL;

	size -= SKB_DATA_ALIGN(sizeof(struct skb_shared_info));

	/* Assumes caller memset cleared SKB */
	skb->truesize = SKB_TRUESIZE(size);
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 13, 0)
	atomic_set(&skb->users, 1);
#else
	refcount_set(&skb->users, 1);
#endif
	skb->head = data;
	skb->data = data;
	skb_reset_tail_pointer(skb);
	skb->end = skb->tail + size;
	skb->mac_header = (typeof(skb->mac_header))~0U;
	skb->transport_header = (typeof(skb->transport_header))~0U;

	/* make sure we initialize shinfo sequentially */
	shinfo = skb_shinfo(skb);
	memset(shinfo, 0, offsetof(struct skb_shared_info, dataref));
	atomic_set(&shinfo->dataref, 1);

	if (frag_size) {
		skb->head_frag = 1;
		if (page_is_pfmemalloc(virt_to_head_page(data)))
			skb->pfmemalloc = 1;
	}

	return skb;
}
EXPORT_SYMBOL(build_skb_around);
#endif

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 7, 0)
static DEFINE_KSYM_PTR(kallsyms_lookup_name);

static __init int get_addr_kallsyms_lookup_name(void)
{
	char sym[KSYM_SYMBOL_LEN], *op, *sp;
	unsigned long addr, off, sz;

	for (addr = (unsigned long)sprint_symbol - 0x8000;
	     addr < (unsigned long)sprint_symbol + 0x8000;
	     addr = addr - off + sz) {
		sprint_symbol(sym, addr);
		sp = sym;
		if (!strsep(&sp, "+") || !(op = strsep(&sp, "/"))
		||  !(sz = simple_strtoul(sp, NULL, 0))) {
			break;
		}
		if (!(off = simple_strtoul(op, NULL, 0))
		&&  !strcmp(sym, "kallsyms_lookup_name")) {
			ksym_kallsyms_lookup_name = (void *)addr;
			return 0;
		}
	}

	return -ENOSYS;
}

unsigned long kallsyms_lookup_name(const char *name)
{
	return CALL_KSYM_PTR(kallsyms_lookup_name, name);
}
EXPORT_SYMBOL(kallsyms_lookup_name);
#else
#define get_addr_kallsyms_lookup_name()		(0)
#endif

#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 3, 0)
struct fwnode_handle *fwnode_find_reference(const struct fwnode_handle *fwnode,
		      const char *name,
		      unsigned int index)
{
	int rc;
	struct fwnode_handle *fwnode_ref;

#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 14, 0)
	if (is_of_node((void *)fwnode)) {
		struct of_phandle_args of_args;
		rc = of_parse_phandle_with_args(to_of_node((void *)fwnode), name, NULL, index, &of_args);
		fwnode_ref = rc ? NULL : of_fwnode_handle(of_args.np);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 9, 0)
	} else if (is_acpi_node((void *)fwnode)) {
		struct acpi_reference_args acpi_args;
		rc = acpi_node_get_property_reference(fwnode, name, index, &acpi_args);
		fwnode_ref = rc ? NULL : acpi_fwnode_handle(acpi_args.adev);
#endif
	} else {
		rc = -ENOSYS;
		fwnode_ref = NULL;
	}
#else
	struct fwnode_reference_args args;

	rc = fwnode_property_get_reference_args(fwnode, name, NULL, 0, index, &args);
	fwnode_ref = args.fwnode;
#endif
	return rc ? ERR_PTR(rc) : fwnode_ref;
}
EXPORT_SYMBOL(fwnode_find_reference);
#endif

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 12, 0)
#if IS_ENABLED(CONFIG_OF_MDIO)
struct phy_device *of_phy_attach(struct net_device *dev,
				 struct device_node *phy_np,
				 u32 flags, phy_interface_t iface)
{
	struct phy_device *phy = of_phy_find_device(phy_np);
	int ret;

	if (!phy)
		return NULL;

	ret = phy_attach_direct(dev, phy, flags, iface);

	/* refcount is held by phy_connect_direct() on success */
	put_device(&phy->mdio.dev);

	return ret ? NULL : phy;
}
EXPORT_SYMBOL(of_phy_attach);
#endif
#endif

#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 14, 0)
#if IS_ENABLED(CONFIG_PHYLIB)
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 5, 0)
struct phy_device *__fwnode_phy_find_device(struct fwnode_handle *phy_fwnode)
{
	struct device *d;

	if (!phy_fwnode)
		return NULL;

	d = bus_find_device_by_fwnode(&mdio_bus_type, phy_fwnode);
	if (!d)
		return NULL;

	return to_phy_device(d);
}
#else
struct phy_device *__fwnode_phy_find_device(struct fwnode_handle *phy_fwnode)
{
	struct device *d;
	struct mdio_device *mdiodev;

	if (!phy_fwnode)
		return NULL;

	d = bus_find_device_by_fwnode(&mdio_bus_type, phy_fwnode);
	if (!d)
		return NULL;

	mdiodev = to_mdio_device(d);

	if (mdiodev->flags & MDIO_DEVICE_FLAG_PHY)
		return to_phy_device(&mdiodev->dev);

	put_device(&mdiodev->dev);

	return NULL;
}
#endif
EXPORT_SYMBOL(__fwnode_phy_find_device);

struct fwnode_handle *__fwnode_get_phy_node(struct fwnode_handle *fwnode)
{
	struct fwnode_handle *phy_node;

	/* Only phy-handle is used for ACPI */
	phy_node = fwnode_find_reference(fwnode, "phy-handle", 0);
	if (is_acpi_node(fwnode) || !IS_ERR(phy_node))
		return phy_node;
	phy_node = fwnode_find_reference(fwnode, "phy", 0);
	if (IS_ERR(phy_node))
		phy_node = fwnode_find_reference(fwnode, "phy-device", 0);
	return phy_node;
}
EXPORT_SYMBOL(__fwnode_get_phy_node);
#endif

#ifdef CONFIG_ACPI
int __acpi_get_local_address(acpi_handle handle, u32 *addr)
{
	unsigned long long adr;
	acpi_status status;

	status = acpi_evaluate_integer(handle, METHOD_NAME__ADR, NULL, &adr);
	if (ACPI_FAILURE(status))
		return -ENODATA;

	*addr = (u32)adr;
	return 0;
}
EXPORT_SYMBOL(__acpi_get_local_address);
#endif
#endif

int __init compat_init(void)
{
	int rc;

	if ((rc = get_addr_kallsyms_lookup_name())) {
		pr_err("Failed to get address of \"kallsyms_lookup_name\"\n");
		goto err;
	}

	return 0;
err:
	return rc;
}
